﻿#region 참조 네임스페이스

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;

#endregion

namespace Skying.Net.NetworkInformation
{
	/// <summary>
	/// Wake On LAN 명세에 따라 매직 패킷을 구성하여 장치 원격 전원 제어를 위한 인터페이스를 제공한다.
	/// </summary>
	public static class WakeOnLan
	{
		#region 상수

		/// <summary>
		/// 매직패킷의 헤더 상수를 나타낸다.
		/// </summary>
		private static readonly byte[] _HEADER = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };

		/// <summary>
		/// 매직패킷 전송을 위한 기본 포트 번호 상수를 나타낸다.
		/// </summary>
		private const int _WOL_DEFAULT_PORT = 0x2fff;

		/// <summary>
		/// 매직패킷 내에 구성되는 물리주소의 개수 상수를 나타낸다.
		/// </summary>
		private const int _MAC_ADDRESS_COUNT = 16;

		#endregion

		#region Wake On LAN

		/// <summary>
		/// Wake On LAN을 위한 매직패킷을 생성한다.
		/// </summary>
		/// <param name="macAddress">매직패킷의 대상 물리 주소를 설정한다.</param>
		/// <returns>주어진 물리 주소에 대한 매직패킷이 반환된다.</returns>
		/// <exception cref="System.ArgumentException">주어진 물리 주소('-' 구분자가 제거된)가 올바르지 않은 경우 발생한다.</exception>
		private static byte[] CreateMagicPacket(string macAddress)
		{
			byte[] buffer = new byte[102];

			// 버퍼에 헤더를 복사한다.
			_HEADER.CopyTo(buffer, 0);

			int count = _HEADER.Length;

			macAddress = macAddress.Replace("-", string.Empty);

			if (macAddress.Length != 12)
			{
				throw new ArgumentException("The lenght of MAC Address should be 12.");
			}

			// 물리 주소를 byte 배열로 변환한다.
			byte[] macAddressBinary = Enumerable.Range(0, macAddress.Length)
				.Where(t => t % 2 == 0)
				.Select(t => Convert.ToByte(macAddress.Substring(t, 2), 16))
				.ToArray();

			for (int index = 0; index < _MAC_ADDRESS_COUNT; index++)
			{
				macAddressBinary.CopyTo(buffer, count);

				count += macAddressBinary.Length;
			}

			return buffer;
		}

		/// <summary>
		/// 물리 주소에 대한 매직 패킷을 특정 주소로 전송한다.
		/// </summary>
		/// <param name="macAddress">매직 패킷에 대한 대상 물리 주소를 설정한다.</param>
		/// <param name="ipAddress">매직 패킷을 수신할 대상의 네트워크 주소를 설정한다. 주소가 정해지지 않을 경우, 브로드캐스트 주소를 사용한다.</param>
		public static void WakeUp(string macAddress, IPAddress ipAddress = null)
		{
			byte[] magicPacket = CreateMagicPacket(macAddress);

			new UdpClient().Send(magicPacket, magicPacket.Length, new IPEndPoint(ipAddress == null ? IPAddress.Broadcast : ipAddress, _WOL_DEFAULT_PORT));
		}

		/// <summary>
		/// 물리 주소에 대한 매직 패킷을 특정 주소로 전송한다.
		/// </summary>
		/// <param name="macAddress">매직 패킷에 대한 대상 물리 주소를 설정한다.</param>
		/// <param name="ipAddress">매직 패킷을 수신할 대상의 네트워크 주소를 설정한다.</param>
		public static void WakeUp(string macAddress, string ipAddress)
		{
			WakeUp(macAddress, IPAddress.Parse(ipAddress));
		}

		/// <summary>
		/// 물리 주소에 대한 매직 패킷을 특정 주소에 대한 서브넷 망 내 브로드캐스트 주소로 전송한다.
		/// </summary>
		/// <param name="macAddress">매직 패킷에 대한 대상 물리 주소를 설정한다.</param>
		/// <param name="ipAddress">매직 패킷을 수신할 대상의 네트워크 주소를 설정한다.</param>
		/// <param name="subnetMask">매직 패킷을 수신할 대상의 서브넷 주소를 설정한다.</param>
		public static void WakeUp(string macAddress, string ipAddress, string subnetMask)
		{
			long[] ipParts = ipAddress.Split('.').Select(t => Convert.ToInt64(t)).ToArray();
			long[] subnetParts = subnetMask.Split('.').Select(t => Convert.ToInt64(t)).ToArray();

			// 서브넷 마스크의 각 부의 비트를 반전시킨다. 예를 들어, 255이면 2진수로 11111111이므로 결과는 00000000이 된다.
			long[] invertedSubnetParts = subnetMask.Split('.')
				.Select(m =>
					Convert.ToInt64(new string(string.Format("{0,8:00000000}", int.Parse(Convert.ToString(Convert.ToInt64(m), 2)))
						.Select(n =>
							n.Equals('0') ? '1' : '0')
							.ToArray()), 2))
				.ToArray();

			// 비트를 반전시킨 서브넷 마스크와 각 아이피 부를 OR 연산시켜 SDB(subnet directed broadcasts)를 알아낸다.
			string toAddress = string.Format("{0}.{1}.{2}.{3}",
				ipParts[0] | invertedSubnetParts[0],
				ipParts[1] | invertedSubnetParts[1],
				ipParts[2] | invertedSubnetParts[2],
				ipParts[3] | invertedSubnetParts[3]);

			WakeUp(macAddress, IPAddress.Parse(toAddress));
		}

		#endregion
	}
}